from visa import instrument, VisaIOError # PyVISA
import visapatchref
import numpy as np
import time
from sys import stdout
#import string

"""
--------------------------------------------------------------------
Class to control the basic operation of the 7260 or 7265 lock-in amplifier.  

It is worth noting that despite the number of lines of code here (much being 
documentation text copied from the 7260 instrument manual), the LIA is a
particularly powerful instrument and there are countless commands not covered
here. If you need something, check out the manual (/public/Manuals/Lock-in amplifier/)
and make it happen.

Note the instrument has an option to respond to queries in either standard
mode or floating-point mode.  Appending a period (.) to the end of a command
selects a floating-point response.  E.g. the command instrument.ask('SEN') will 
return a string '0'-'27' representing the sensitivity lookup table used to 
choose sensitivity level, but instrument.ask('SEN.') will return a floating-point 
value of the sensitivity setting in V or A, which is usually more useful.

If you have communication issues, besides the usual things like verifying the
GPIB address and USB-GPIB functionality, it wouldn't hurt to be sure the 
instrument is set for '[CR],[LF]' termination characters (Menu...Communications
...GPIB Options).  This is the default for PyVISA last time I checked (a setting
accessed or changed via the .term_chars attribute of "instrument"; if blank 
then it's set to the default.)  
See PyVISA documentation http://pyvisa.sourceforge.net/pyvisa/node18.html.
 
 
 My general convention (some based on Python convention...):
 
   .__function__()  built-in/reserved methods
   ._variable_      variables that are not callable...i.e. no '()' at end
   .InternalFunc()  initial capital letter indicates the function is called internally only
   .someCommand()   perform instrument commands/queries
   .MACRO_FUNC()    "macro" functions that do a whole bunch of things, like set up the instrument


Revision history:
12 Aug 2010, lnt
- Changed refFreq to output frequency in floating point units
- Added dacLevel function to control the DAC output voltage level
- Added autoMeasure function to initiate an auto measure operation
11 Aug 2010, lnt
- Changed reset function to allow a reset from a visapatched script: this removes dependence
    on the status byte
10 Aug 2010, lnt
- Adjusted initialization to allow "visapatching" for use with the Prologix controllers
    The visapatch class used mimicks the visa functions and allows "seemless" communion
    with the rest of the lockin classes
4 Aug 2010, ntg, Laptop
- What a disaster. Yesterday the 7260 ceased to accept GPIB commmands. Strangely,
  this occured only within Python only with the USB-GPIB adapter (not in Python using
  the PCI GPIB, and not outside of Python with the USB-GPIB, i.e. the CEC-488 
  TRTEST.EXE.
  The "solution": instance the instrument, then cycle the GPIB address from the LIA
  front panel, e.g. up to 13 then back down to 12.  Then it's all good.  SO BIZARRE!
  I added a new function _shyModeFunc() to tell the user to do this.
- Now catching comm errors in __init__ using the visa.VisaIOError exception.
- GPIB termination characters explicitly set to '\r\n' instead of relying on 
  default.
- VISA timeout reduced from 10. to 2. seconds. No longer a separate custom attribute.
- Polling of status byte .raw.stb LSB using .pollStatusByte; now can use instrument's
  auto-xxx functions because we know when it is complete.
- While I was at it, added .overload, .refUnlock, and .dataReadyBit based on stb.
- And BTW, serial is never going to work so removed. Have you read that section 
  of the manual? What a mess.
- Some tidying up of code. Spacing added, "!=None" replaced by "is not None",
  type(obj) == type(example) replaced by isinstance(obj, class), etc.
31 July 2010, ntg, Augustus
- Minor changes to string printing.
- New .getXY_wait3tau
29 July 2010, ntg, Augustus
- Typo in the .tc docstring fixed.
- New .sens_dict (voltage only) and .tc_dict dictionaries, and complements 
  .sens_revDict and .tc_revDict.
- Now .sensitivity(N1,[N2]) can handle floating-point voltage input(s) as well
  as integer code(s).  Minor change to .SetOrCheckSomething() in user notification.
- Similar change to .tc(N1,[N2]); can now handle floating-point time constant
  input as an alternative.
13 July 2010, ntg, Augustus
- Addition of self.slope(1) call after a reset to put filter on 12 dB/oct.
- New warnings in .getR(N,[False]) and getR_theta(N,[False]) with option to turn 
  the warnings off by changing False to True.
- Added .lightsOut() to toggle the LEDs and display backlight
- Auto-sensitivity is troubling as implemented but would be helpful so I wrote 
  a quick internal function to do it in-module rather than in-instrument.
- Added .refPhase(), to allow nulling of quadrature (Y)
12 July 2010, ntg, Augustus
- Minor change to the "model7265" mode documentation pointing to appendix F.
- Minor bug fix in .refHarmonic()
- Tweaks to .sensitivity() 'auto' setting since it likes to time out
10 July 2010, ntg, Augustus
- Created.  Includes some very decent dual harmonic mode support in nearly
  every function. Works well over GPIB.

--
USAGE EXAMPLE
 # For operation over GPIB address N, just pass it the address number

 from lockin import lia726x         # import the lia726x class from this lockin module

 lia = lia726x(N)                   # for GPIB channel N (defaults to 12 if left blank)
 #lia = lia726x(n, useSerial=True)  # for COMn operation (UNTESTED)
 
 lia.reset() # reset to defaults, always a good idea
 
 ## Basic setup of the reference channel
 lia.refMode(0)      # 0 = single reference (typical), 1 = dual harmonic, 2 = dual reference (internal AND external)
 # ...wait a sec if you change it
 lia.refInternal()   # use internal oscillator
 #lia.refExternal()  # or external connected to REF IN
 lia.refHarmonic(m1, [m2]) # set the modes you want to look at (m2 for dual harmonic mode) 
 lia.oscFreq(f_Hz); lia.oscAmplitude(VorI_rms) # IF you're using internal oscillator
 
 ## Basic setup the signal channel
 lia.currentInputMode(1)  # use the B current input connector in either 1 = high-bandwidth or 2 = low-noise mode
 #lia.voltageInputMode(1) # ...or voltage mode. 1 = A input, 2 = -B input, 3 = A-B
 lia.tc(integerCode)    # set time constant; the code<->time constant is in the .tc() doc string
 lia.tc()               # see what you just put in, but this time in seconds
 lia.sensitivity(integerCode) # set input sensitivity in V/ or A/; similar table deal
 #lia.sensitivity('auto') # perhaps a slicker way...
 #lia.sensitivity() # see what code it landed on and set it to that 
 lia.slope(integerCode) # set the filter rolloff; similar table deal
 lia.acGain(0)          # this is in integer bels, as in 1 bel = 10 dB
 #lia.acGain_auto(1)    # ...or turn on auto ac gain
 # and so on...
 
 ## Get some data
 lia.getR_theta(Nmeas) # get "Nmeas" samples of magnitude and theta
 #lia.getXY(Nmeas)     # ...or X, Y if you prefer (R = sqrt(X^2+Y^2), theta=tan(Y/X) or something...)
 
 lia.closeport()
 
 lia.openport() # re-open the port later (can't change address this way though)
--

"""

def _shyModeFunc():
    """
    """
    finalCountdown = 15  # (s)
    print('\nThe LIA has been shy lately. \n'+\
          'Is the GPIB address correct? Default is 12.\n'+\
          'If so, cycle the GPIB address up and down using the front panel, \n'+\
          '  e.g. to 13 then back to 12:'+\
          '\n\t* MENU'+\
          '\n\t* COMMUNICATIONS'+\
          '\n\t* GPIB OPTIONS'+\
          '\n\t* ADDRESS = xx, push softkey UP then DOWN.'+\
          '\n\t* Wait for countdown to expire...\n'+\
          '\n(Or press CTRL-C to abort.)')
    # Give user some time to read the above message
    time.sleep(7)
    print('\n* You have '+str(finalCountdown)+' seconds... * (it may take a moment for LIA buttons to respond)\n')
    # Initiate a countdown visible to the user.
    tN = finalCountdown
    while tN >= 0:
        stdout.write('\t'+str(tN)+'  \r') # print repeatedly to same line
        tN -= 1
        time.sleep(1)
    print('\n\nWell hopefully that worked. It did if you don\'t see a serious error.')

## ****************************************************************************
##  A class for the 7260 or 7265 lock-in amplifier
## ****************************************************************************
class lia726x():

    def __init__(self, addr=12, comclass=None):
        """
        Initialize the instrument. Assumes GPIB operation on address 12. Input
        addr as needed.
                
        Information on the instrument itself is in the group wiki at (when on 
        ECE network) http://joey01.ece.umn.edu/wiki/EG%26G_7260_lock-in_amplifier
        """
        self._GPIBaddr_ = None
        # If the address is an integer between 0 and 30 we will try to instance it.
        if comclass:
            self.raw = visapatchref.Visapatch_Serial(addr, comclass)
            self._idn_ = self.raw.ask('id')
            self._dualHarm_ = None # initialize
            self._dualRef_ = None
            blah = self.refMode() # this will set those two attributes (and blah assign prevents it from returning its value to shell)
            return
        elif (isinstance(addr, int)) & ((addr >= 0) & (addr <= 30)): # GPIB (making sure address in integer and in range)
            self._GPIBaddr_ = addr
            self.raw = instrument('GPIB::' + str(self._GPIBaddr_) + '::INSTR', \
                                  timeout=2., term_chars = '\r\n')
            print('LIA instance on GPIB address '+str(addr))
        else:
            print('wtf?')
        ## Test communication
        try:
            self.raw.write('ID')          # this is the LIA's version of *idn?
        except VisaIOError:
            # Prompt user (possibly for 2nd time) to fix, and then try one more time
            print('\nVisaIOError encountered upon first cmd written to lockin.\n')
            _shyModeFunc()
            self.raw.write('ID')
        ## We really do want this value, though
        try:
            self._idn_ = self.raw.read()
        except VisaIOError:
            print('\nVisaIOError encountered trying to read lockin IDN.\n')
            time.sleep(self.raw.timeout)      # pause
            self._idn_ = self.raw.ask('IDN')  # ask for it again, and fail hard this time
        self._model7265_ = self._idn_.__contains__('7265')
        if self._model7265_:
            # I'm not sure there's much to do here. A skim through Appendix F
            # of the 7265 manual indicates that the changes are relatively minor
            print('Sorry, the 7265 feature does nothing at the moment.\n'+\
                  'It may work as is; see Appendix F of the 7265 manual.')
        ## Get some values we need by querying the instrument
        # Are we in dual harmonic or dual reference mode?
        self._dualHarm_ = None # initialize
        self._dualRef_ = None
        blah = self.refMode() # this will set those two attributes (and blah assign prevents it from returning its value to shell)
        
    def openport(self):
        """
        Re-open the port
        """
        if self._GPIBaddr_ is not None: # we're using GPIB
            self.__init__(self._GPIBaddr_)
        else: # we're using serial (hopefully)
            print('Can\'t re-open the port. Where did the GPIB address go?')

    def closeport(self):
        self.raw.close()

    def reset(self,comtype=None):
        """
        Reset instrument to default settings, mostly.
        """
        if comtype=='COM':
            self.raw.write('ADF')
            time.sleep(15)
        else:
            print('Instrument resetting to defaults...')
            self.raw.write('ADF')
            self._dualHarm_ = False;  self._dualRef_ = False # we went back to single ref mode
            #time.sleep(3) # give it a few seconds
            self.pollStatusByte()  # wait for status byte LSB to -> 1
        print('Changing output filter slope from default 6 -> recommended 12 dB/oct.')
        self.slope(1) # the default output filter slope value is generally too small
        
    def SetOrCheckSomething(self,cmdString='IMODE',setValue=None, minValue=None, maxValue=None, intOnly=False):
        """
        Internal function.
        Use the input "cmdString" to send that command to instrument with 
        optional variable "setValue".  
        
        If you leave setValue blank, the present value is queried and returned.
                
        Optionally, you can include a minValue and/or maxValue to keep the 
        final command string sensical.  If you want to further limit this to
        integers, set intOnly=True.
        """
        if setValue is None: # no value, just the command string to query
            resp = self.raw.ask(cmdString) 
        else:
            # If requested but not an integer already
            if intOnly & (not(isinstance(setValue, int))): 
                setValue = int(setValue) # ...we need to limit setValue to an integer
                print('Your input value was truncated to be an integer, '+str(setValue)+'.')
            # Either way make sure it is an integer or float before doing numerical comparison
            if (isinstance(setValue, int)) | (isinstance(setValue, float)):
                if (minValue is not None)&(setValue < minValue):
                    setValue=minValue
                    print('Input out of range (< minimum), but I sort of took care of it.')
                if (maxValue is not None) & (setValue > maxValue):
                    setValue = maxValue
                    print('Input out of range (> maximum), but I sort of took care of it.')
            resp = None
            self.raw.write(cmdString + ' ' + str(setValue))
        return resp
    
    def lightsOut(self):
        """
        Toggles the LEDs and LCD backlight on and off.
        """
        resp = self.raw.ask('LTS') # see if the lights are on or off right now
        # convert '0'/'1' to int, invert (yields True/False), 
        # convert back to 0/1, then convert to string and write to instrument.
        self.raw.write('LTS ' + str(int(not(int(resp))))) 
    
    def currentInputMode(self, n=None):
        """
        n Input mode
        0 Current mode off - voltage mode input enabled
        1 High bandwidth (HB) current mode enabled - connect signal to B input connector
        2 Low noise (LN) current mode enabled - connect signal to B input connector
        """
        resp = self.SetOrCheckSomething('IMODE', n, 0, 2, True)
        return resp
    
    def voltageInputMode(self, n=None):
        """
        The value of "n" sets up the input configuration according to the following table:
        n    Input configuration
        0       Both inputs grounded (test mode)
        1       A input only
        2       -B input only
        3       A-B differential mode
        """
        if n is not None: # if we're actually doing something in voltage mode 
            self.currentInputMode(0) # ...we must turn off current mode first
        resp = self.SetOrCheckSomething('VMODE', n, 0, 3, True)
        return resp

    def voltageInputDevice(self, n=None):
        """
        FET [n] Voltage mode input device control
        The value of n selects the input device according to the following table:
        
        n Selection
        0 Bipolar device, 10 kOhm input impedance, 2 nV/sqrt(Hz) voltage noise at 1 kHz
        1 FET, 10 MOhm input impedance, 5 nV/sqrt(Hz) voltage noise at 1 kHz
        """
        if n is not None: # if we're actually doing something in voltage mode...
            self.currentInputMode(0) # ...must turn off current mode first or this setting means nothing
        resp = self.SetOrCheckSomething('FET', n, 0, 1, True)
        if n is not None:
            if n == 0:
                print('BJT input selected. Be careful.\n'+\
                'Lower voltage noise, but also much lower input impedance than FET.')
        return resp
        
    def bjt_input(self):
        """
        A convenience function, to use the Bipolar input rather than FET 
        (applicable in voltage mode only)
        """
        resp = self.voltageInputDevice(0)
        return resp

    def fet_input(self):
        """
        A convenience function, to use the FET input rather than bipolar\
        (applicable in voltage mode only)
        """
        resp = self.voltageInputDevice(1)
        return resp

    def inputShield(self, n=None):
        """
        FLOAT [n] Input connector shield float/ground control
        The value of n sets the input connector shield switch according to the 
        following table:
        
        n Selection
        0 Ground
        1 Float (connected to ground via a 1 kOhm resistor)
        """
        resp = self.SetOrCheckSomething('FLOAT', n, 0, 1, True)
        return resp

    def floatTheInputShield(self):
        """
        Use this in most cases; a 1 kOhm resistor separates the shield of the 
        input connector(s) from instrument ground.  
        
        Also see .groundInputShield()
        """
        resp = self.inputShield(1)
        return resp

    def groundTheInputShield(self):
        """
        Use this if your circuit is not otherwise grounded.
        
        Also see .floatInputShield()
        """
        resp = self.inputShield(0)
        print('Input connector shield(s) grounded. Watch out for ground loops!')
        return resp

    def coupling(self, n=None):
        """
        CP [n] Input coupling
        The value of n sets the input coupling mode according to the 
        following table:
        
        n Coupling mode
        0 AC
        1 DC
        """
        resp = self.SetOrCheckSomething('CP', n, 0, 1, True)
        if n == 1:
            print('Really, DC coupling? Are you sure? Very low frequency only.')
        return resp

    def lineFilter(self, n=None, checkFor50Hz=False):
        """
        LF [n] Signal channel line frequency rejection filter control
        
        In instruments manufactured after June 1996:
        
            n   Selection
            0   Off
            1   Enable 50 or 60 Hz notch filter
            2   Enable 100 or 120 Hz notch filter
            3   Enable both filters
        
        *Our 7260 was manufactured after 1996, but not sure about Leger's 7265*.
        
        
        In instruments manufactured before June 1996:
            0   Off
            1   On (i.e. reject 50/60 Hz and 100/120 Hz)
        
        Users may identify which version of the instrument they have by sending the
        command 'LF 3'; if this is accepted by the instrument, it was made after June 1996,
        but if it generates a command error, it was made prior to this date.
        Additionally units made after June 1996 respond to the command 'LINE50', which
        sets the notch filter center frequency.
        
        This function is set up to send that 'LINE50' command if you set 
        checkFor50Hz=True. You should never change to 50 Hz, so this function
        won't let you do that; it just lets you choose 60(50), 120(100), or both.
        """
        if checkFor50Hz:
            print("Checking that we're on 60/120 Hz, not 50/100 Hz.\n"+\
                  "A '0' returned is good. Welcome to America.\n\n"+\
                  "A comm error is good too, just means instr is older (see function help).\n\n"+\
                  "A '1' retured is BAD unless you are in Europe or Asia.")
            resp = self.raw.ask('LINE50')
        else:
            resp = self.SetOrCheckSomething('LF', n, 0, 3, True)
        return resp

    def vectorVoltmeter(self, n=None):
        """
        RANGE [n] Signal Recovery/Vector Voltmeter mode selector

        n Mode
        0 Signal Recovery
        1 Vector Voltmeter
        
        NOTE: Instrument always reverts to signal recovery mode (n=0) on power-up.
        
        Vector voltmeter mode is better at measuring phase but is not as 
        good at rejecting noise from outside the reference frequency.
        
        DO NOT use vector voltmeter mode if you are using either the dual 
        harmonic mode or the dual reference mode (manual pp. 3-1 and 3-2, i.e. 
        PDF pages 20 and 21).
        """
        if n:
            print("Welcome to vector voltmeter mode, I hope you know what you're doing.")
            if self._dualHarm_ | self._dualRef_:
                print("Use of vector voltmeter mode is NOT recommended with dual harmonic or reference mode.")
        resp = self.SetOrCheckSomething('RANGE', n, 0, 1, True)
        return resp

    def overload(self):
        """
        Tells you wheter the instrument input is overloaded.  Returns 1 if
        overload or 0 if not.
        
        Note the threshold here is a bit of an early-warning system. The front 
        panel says overload >= 300% of full scale, but this bit goes high 
        when >= 120% of full scale.
        """
        # Poll the status byte
        statByte = self.raw.stb
        # If the value is > 16, then there is a chance that bit 4 is 1
        if statByte >= 16:
            # Convert to binary and grab bit 4, then back to integer.
            overloadBit = int(bin(statByte)[-5])
        else:
            overloadBit = 0
        return overloadBit

    def refUnlock(self):
        """
        Checks bit 3 of the status byte to see if the reference has lost lock.  
        Returns 1 if unlocked or 0 if not.
        """
        # Poll the status byte
        statByte = self.raw.stb
        # If the value is >= 8, then there is a chance that bit 3 is 1
        if statByte >= 8:
            # Convert to binary and grab bit 3, then back to integer.
            unlockBit = int(bin(statByte)[-4])
        else:
            unlockBit = 0
        return unlockBit

    def dataAvailable(self):
        """
        Checks bit 7 of the status byte to see if new data is available.  
        Returns 1 if so or 0 if not.
        """
        # Poll the status byte
        statByte = self.raw.stb
        # If the value is >= 128, then we know the MSB of this byte is high.
        if statByte >= 128:
            dataReadyBit = 1
            ## Convert to binary and grab bit 7, then back to integer.
            #unlockBit = int(bin(statByte)[-8])
        else:
            dataReadyBit = 0
        return unlockBit

    def pollStatusByte(self, watchdogTime_s=45):
        """
        Polls status byte to see if the last command has completed.
        
        Checks status byte once every couple seconds until its LSB is 1, 
        or until watchdogTime_s elapses.
        
        The instance has a status byte on attribute .raw.stb.  This is a decimal
        value from 0 to 255: (manual p. 91 a.k.a. 5-43)
        
        Bit  Decimal Value      Status Byte
              (if alone)
        0       1               **command complete** <- this is the one we want
        1       2               invalid command
        2       4               command parameter error
        3       8               reference unlock
        4       16              overload
        5       32              new ADC values available after external trigger
        6       64              asserted SRQ
        7       128             data available
        
        For example, bit 0 changes from 0 to 1 when the LIA finishes an 
        auto-sensitivity operation.
        """
        t = watchdogTime_s
        def grabLSB(intNum):
            # Convert to binary string and grab the LSB
            theLSB = int(bin(intNum)[-1])
            return theLSB
        # Get the least-significant bit of status byte
        theLSB = grabLSB(int(self.raw.stb))
        # Check once every 2 seconds until watchdog timer elapses
        while (t >= 0) & (theLSB != 1):
            time.sleep(2)
            statusByte = self.raw.stb
            theLSB = grabLSB(int(statusByte))
            stdout.write('Status byte = ' + str(statusByte) + \
                         '-> LSB ("cmd complete") = ' + str(theLSB) + \
                         '   (watchdog time: ' + str(t) + ')\r')
            t -= 2

    def AutoSensitivity(self, noneOneOrTwo=''):
        """
        This function is meant to be called from .sensitivity() using 'auto' for
        one or both inputs as applicable.  It is not robust on its own.
        
        The auto-sensitivity function in the instrument itself, 'AS' (or 'AS1'/'2'
        in dual-mode), leaves a lot to be desired. It is prone to cause timeout
        errors due to the uncertain nature of finding the desired setting.  It is
        also not that great (from the manual 3.3.19, pp. 36-7):
        
            During application of several of the auto functions, decisions are made 
            on the basis of output readings made at a particular moment. Where this 
            is the case, it is important for the output time constant set by the 
            user to be long enough to reduce the output noise to a sufficiently low 
            level so that valid decisions can be made and that sufficient time is 
            allowed for the output to settle...
            ...In the presence of noise, or a time-varying input signal, it may be a 
            long time before the Auto-Sensitivity sequence comes to an end, and the 
            resulting setting may not be what is really required.
            
        The dependence on proper time constant setting does not go away.
        """
        useDual = noneOneOrTwo == '2' # if we're using dual and setting the second one            
        if useDual:
            print('\nSetting sensitivity of dual ref/harm channel 2...')
        n = 27 # initialize; start at lowest sensitivity
        notSatisfied = True # initialize
        # Get the existing time constant in s
        pres_tc = float(self.raw.ask('TC' + noneOneOrTwo + '.')) 
        print('Instrument time constant is ' + str(pres_tc) + \
              ' s; this greatly affects success of auto sens.')
        # Gradually increase sensitivity until we measure 50% of full-scale
        while (n > 1) & (notSatisfied):
            if useDual:
                self.sensitivity(n2=n)  # we're setting the second reference
            else:
                self.sensitivity(n)
            time.sleep(3. * pres_tc)  # wait three time constants
            # Get the sensitivity value we just set in Vrms or Arms
            fullScale = self.raw.ask('SEN' + noneOneOrTwo + '.')  # '.' for floating-point mode
            print('Trying ' + fullScale + ' Vrms or Irms...')
            # Get a set of magnitude measurement
            mag = self.getR(10, allowWarn=False) 
            # If measured signal mag at least 50% of full-scale setting
            if np.median(mag) > (0.5 * float(fullScale)): 
                ppNoise = (np.max(mag) - np.min(mag)) / np.mean(mag) # approximate fractional "noise"
                # We've settled, but warn the user the signal is noisy
                if np.abs(ppNoise) > 0.10: 
                    print('Settled on ' + str(fullScale) + ' sens., but signal mag. fluctuation > 10%'+\
                          ' of mean signal mag (after pausing 3 time constants).'+\
                          '\nConsider increasing the time constant.')
                else:
                    print('Settled on setting ' + str(n) + '; < 10% fluctuation in signal magnitude.')
                # we're done either way
                notSatisfied = False
            else:
                # decrement counter (i.e. try a higher sensitivity)
                n -= 1 
        return n # return the value of n that was settled on

    def sensitivity(self, n=None, n2=None):
        """
        SEN [n]
        Full-scale sensitivity control
        
        The value of n sets the full-scale sensitivity depending on the setting 
        of the IMODE control:
        
        n='auto' will find the "best" sensitivity setting. Note that this takes
        some time, so we wait 10 seconds (for each harmonic/reference) to try
        to avoid writing to the instrument again before it is ready.
        
        Same goes for the n2 input, in dual harmonic or reference mode.
        
        If you want to give it a go, you can try to input a floating-point 
        full-scale sensitivity value directly, and this will look it up.  Works
        for voltage mode only at the moment.
        
        (To convert a floating point *voltage senstivity* to a code, use 
        the .sens_dict function: ".sens_dict()[fp_vsens]".  Current mode
        lookup not yet implemented there.)
        
            full-scale sensitivity
        n   IMODE=0 IMODE=1 IMODE=2
        1   2 nV	2 fA	n/a
        2   5 nV	5 fA    n/a
        3   10 nV	10 fA	n/a
        4   20 nV	20 fA	n/a
        5   50 nV	50 fA	n/a
        6   100 nV	100 fA	n/a
        7   200 nV	200 fA	2 fA
        8   500 nV	500 fA	5 fA
        9   1 uV	1 pA	10 fA
        10  2 uV	2 pA	20 fA
        11  5 uV	5 pA	50 fA
        12  10 uV	10 pA	100 fA
        13  20 uV	20 pA	200 fA
        14  50 uV	50 pA	500 fA
        15  100 uV	100 pA	1 pA
        16  200 uV	200 pA	2 pA
        17  500 uV	500 pA	5 pA
        18  1 mV	1 nA	10 pA
        19  2 mV	2 nA	20 pA
        20  5 mV	5 nA	50 pA
        21  10 mV	10 nA	100 pA
        22  20 mV	20 nA	200 pA
        23  50 mV	50 nA	500 pA
        24  100 mV	100 nA	1 nA
        25  200 mV	200 nA	2 nA
        26  500 mV	500 nA	5 nA
        27  1 V         1 uA	10 nA
        
        Floating point mode can only be used for *reading* the sensitivity, 
        which is reported in volts or amps. 
        For example, if IMODE = 0 and the sensitivity is 1 mV, the command 'SEN'
        would report '18' and the command 'SEN.' would report '+1.0E-03'. 
        If IMODE was changed to 1, 'SEN' would still report '18' but 'SEN.' \
        would report '+1.0E-09'.
        """
        def iterator(self, n_or_n2, noneOneOrTwo='', autoWatchdog=60):
            """ Separated this out instead of duplicating the code for n2"""
            resp = None # initialize
            # If the "n_or_n2" input is a string we handle it differently
            if isinstance(n_or_n2, str):
                if n_or_n2.lower() == 'auto': # ...and if it is 'Auto', 'auto', 'AUTO', etc.
                    if self._dualHarm_ | self._dualRef_:
                        #resp = self.AutoSensitivity(noneOneOrTwo) # use my function instead
                        print('Performing auto-sensitivity on harm/ref ' + noneOneOrTwo + '...')
                        self.raw.write('AS' + noneOneOrTwo)
                        # Poll the status byte, waiting for shift from 0 to 1
                        self.pollStatusByte()
                        try:
                            resp = self.raw.ask('SEN' + noneOneOrTwo) # and get a response
                        except VisaIOError:
                            print('Error: The instrument seems to have timed out waiting for auto-sensitivity to finish on Ch '+noneOneOrTwo+'.')
                            return None
                    else:
                        #resp = self.AutoSensitivity() # use my function instead
                        print('Performing auto sensitivity...')
                        self.raw.write('AS') # use auto sensitivity
                        # Poll the status byte, waiting for shift from 0 to 1
                        self.pollStatusByte()
                        try:
                            resp = self.raw.ask('SEN') # and get a response
                        except VisaIOError:
                            print('Error: The instrument probably timed out waiting for auto-sensitivity to finish.')
                            return None
                else: # hmmm... maybe it's an integer
                    try:
                        n_or_n2 = int(n_or_n2)
                        resp = self.SetOrCheckSomething('SEN' + noneOneOrTwo, n_or_n2, 0, 27)
                    except:
                        try:
                            n_or_n2 = self.sens_dict()[float(n_or_n2)]  # try to convert to float and look it up
                            resp = self.SetOrCheckSomething('SEN' + noneOneOrTwo, n_or_n2, 0, 27)
                        except:
                            print('Didn\'t recognize your input "n' + noneOneOrTwo + '", nothing sent.')
            else: # everything is normal, just send n along to the instrument
                # ...but first see if a floating pt type was input
                if isinstance(n_or_n2, float):  
                    try:
                        print('You input a floating point SENS value; assuming it is a voltage or current...')
                        n_or_n2 = self.sens_dict()[n_or_n2]  # try to look it up
                        resp = self.SetOrCheckSomething('SEN'+noneOneOrTwo, n_or_n2, 0, 27)
                    except VisaIOError:
                        print('Nope, I guess not, converting to an integer instead...')
                        pass  # ok, just try the integer
                resp = self.SetOrCheckSomething('SEN'+noneOneOrTwo, n_or_n2, 0, 27, True)
            return resp
        # Do the same thing for n1 or n2, more-or-less
        if self._dualHarm_ | self._dualRef_:
            resp1 = iterator(self, n, '1')
            resp2 = iterator(self, n2, '2')
            resp = str(resp1) + ', ' + str(resp2)
        else:
            resp = iterator(self, n)
        return resp
        
    def autoSensitivity(self, ch=1):
        """
        Calls .sensitivity('auto').  
        
        Auto-sens the dual harmonic or reference using .autoSensitivity(2).
        """
        if self._dualRef_ | self._dualHarm_:
            if ch == 1:
                self.sensitivity('auto')
            elif ch == 2:
                self.sensitivity(None, 'auto')
        else:
            self.sensitivity('auto')

    def sens_dict(self):
        """
        Returns the integer codes corresponding to the floating point voltage
        senstivity, as detailed in the table in the .sensitivity docstring.
        
        As a shortcut to a single integer code, you can use the syntax 
        
            .sens_dict()[V_floatingPt] 
            
        to return the integer code directly.  E.g.
        
            .sens_dict()[100e-3]
            
        will return the integer 24.
        
        * YOU MUST USE FLOATING POINT type as the key if you do this.
        
        The entire dictionary is returned if you don't use a specific key.
        
        (Current mode lookup not yet implemented.)
        """
        Vsens_dict = {2e-9:1, 5e-9:2, 10e-9:3, 20e-9:4, 50e-9:5, 100e-9:6,\
                      200e-9:7, 500e-9:8, 1e-6:9, 2e-6:10, 5e-6:11, 10e-6:12,\
                      20e-6:13, 50e-6:14, 100e-6:15, 200e-6:16, 500e-6:17,\
                      1e-3:18, 2e-3:19, 5e-3:20, 10e-3:21, 20e-3:22, 50e-3:23,
                      100e-3:24, 200e-3:25, 500e-3:26, 1e0:27}
        return Vsens_dict

    def sens_revDict(self):
        """
        Returns the floating point voltage sensitivities corresponding to the 
        integer codes, as detailed in the table in the .sensitivity 
        docstring.
        
        This reverse the dictionary returned by .sens_dict().
        
        As a shortcut to a single integer code, you can use the syntax 
        
            .sens_revDict()[integer_code] 
            
        to return the floating-point voltage sensitivity.  E.g.
        
            .sens_revDict()[24]
            
        will return the value 100e-3.
        
        * YOU MUST USE AN INTEGER TYPE as the key if you do this. 1, not 1.0
        
        The entire reverse dictionary is returned if you don't use a specific key.
        
        (Current mode lookup not yet implemented.)
        """
        sens_dict_fwd = self.sens_dict()  # get the forward dictionary
        sens_dict_rev = {}  # initialize a new reverse dictionary
        for ii in sens_dict_fwd.keys():
            sens_dict_rev[sens_dict_fwd[ii]] = ii  # build the reverse dictionary
        return sens_dict_rev

    def autoMeasure(self):
        self.raw.write('ASM')

    def acGain(self, bels=None):
        """
        Enter the AC gain in integer bels, between 0 and 9 (i.e. between 0 dB
        and 90 dB in 10 dB increments; 10 dB = 1 B)
        """
        resp = self.SetOrCheckSomething('ACGAIN', bels, 0, 9, True)
        return resp

    def acGain_auto(self, autoGain=None):
        """
        0 or False will let the value be set by .acGain(bels)
        1 or True  will continuously adjust AC gain
        blank will return the value
        """
        if autoGain: # note that "None" is interpreted as False according to IF statement
            autoGain=1 # change True to an integer 1
        elif autoGain is not None:
            autoGain=0 # change False to an integer 0
        resp = self.SetOrCheckSomething('AUTOMATIC', autoGain, 0, 1, True)
        return resp

    def slope(self, n=None, n2=None):
        """
        SLOPE [n] Output low-pass filter slope (roll-off) control

            n   Slope
            0   6 dB/octave (default)
            1   12 dB/octave (recommended; p. 33 or 3-15 of manual)
            2   18 dB/octave
            3   24 dB/octave
        
        In dual harmonic or reference modes, they are set separately with n and 
        n2 inputs.  (n2 has no effect in single reference mode.)
        
        Important note from the 7260 manual (3-15): 
          The 6 dB/octave filters are not satisfactory for most purposes because 
          they do not give good rejection of non-random interfering signals which 
          can cause aliasing problems as a result of the sampling process in the 
          main ADC. However, the 6 dB/octave filter finds use where the lock-in 
          amplifier is incorporated in a feedback control loop, and in some 
          situations where the form of the time-domain response is critical. 
          *The user is recommended to use 12 dB/octave unless there is some definite
          reason for not doing so.*
        """
        if self._dualHarm_|self._dualRef_:
            resp1 = self.SetOrCheckSomething('SLOPE1', n, 0, 3, True)
            resp2 = self.SetOrCheckSomething('SLOPE2', n2, 0, 3, True)
            resp = str(resp1)+', '+str(resp2)
        else:
            resp = self.SetOrCheckSomething('SLOPE', n, 0, 3, True)
        return resp

    def tc(self, n=None, n2=None, return_n=False):
        """
        TC [n] Filter time constant control
        
        Leave blank to return the existing value(s) in seconds as a floating-point
        formatted string. (Or to get the n value(s) back, use ".tc(return_n=True)")
        
        Can either enter the integer code(s) or the floating-point time in 
        seconds directly.  The algorithm uses the variable type to distinguish.
        
        (To convert a floating point time in seconds to a code, use 
        the .tc_dict function: ".tc_dict()[fpTime]")

        n   time constant
        0	10 us
        1	20 us
        2	40 us
        3	80 us
        4	160 us
        5	320 us
        6	640 us
        7	5 ms
        8	10 ms
        9	20 ms
        10	50 ms
        11	100 ms
        12	200 ms
        13	500 ms
        14	1 s
        15	2 s
        16	5 s
        17	10 s
        18	20 s
        19	50 s
        20	100 s
        21	200 s
        22	500 s
        23	1 ks
        24	2 ks
        25      5 ks
        26      10 ks
        27      20 ks
        28      50 ks
        29      100 ks
        
        The 'TC.' version of the command can only be used for reading the time 
        constant, and reports the current setting in seconds. Hence if a 'TC 11'
        command were sent, 'TC' would report '11' and 'TC.' would report 
        '1.0E-01', i.e. 0.1 s or 100 ms.
        """
        # if we're writing, it must be 'TC n'
        # ...or if we want just n back, it must be 'TC'
        # otherwise we want to do 'TC.' to get the floating point response
        if self._dualHarm_|self._dualRef_:
            extra_n_str = '1'
        else:
            extra_n_str = ''
        # Handle floating point inputs
        if type(n)==type(100e-3): # if we get a floating point input
            print('Trying to use your floating-point TC input in seconds...')
            try:
                n = self.tc_dict()[n]  # lookup the proper integer code
                print('Success!')
            except:
                print('Didn\'t work. Will be truncated to integer, good luck.')
        if type(n2)==type(100e-3):
            print('Trying to use your floating-point TC2 input in seconds...')
            try:
                n2 = self.tc_dict()[n2]  # lookup the proper integer code
                print('Success!')
            except:
                print('Didn\'t work. Will be truncated to integer, good luck.')
        if (n is not None)|(return_n):
            resp = self.SetOrCheckSomething('TC'+extra_n_str, n, 0, 29, True)
        else:
            resp = self.SetOrCheckSomething('TC'+extra_n_str+'.', None)
        # Now similar for n2, but without the n<->n1 dichotomy
        if self._dualHarm_|self._dualRef_:
            if (n2 is not None)|(return_n):
                resp2 = self.SetOrCheckSomething('TC2', n2, 0, 29, True)
            else:
                resp2 = self.SetOrCheckSomething('TC2.', None)
            resp = str(resp)+','+str(resp2)
        return resp

    def tc_dict(self):
        """
        Returns the integer codes corresponding to the floating point time 
        constant in seconds, as detailed in the table in the .tc docstring.
        
        As a shortcut, you can use the syntax 
        
            .tc_dict()[t_s_floatingPt] 
            
        to return the integer code directly.  E.g.
        
            .tc_dict()[2.]
            
        will return the integer 15.
        
        * YOU MUST USE FLOATING POINT type if you do this
        
        The entire dictionary is returned if you don't use a specific key.
        """
        tc_d = {10e-6:0, 20e-6:1, 40e-6:2, 80e-6:3, 160e-6:4, 320e-6:5, 640e-6:6,\
                5e-3:7, 10e-3:8, 20e-3:9, 50e-3:10, 100e-3:11, 200e-3:12,\
                500e-3:13, 1.:14, 2.:15, 5.:16, 10.:17, 20.:18, 50.:19, 100.:20,\
                200.:21, 500.:22, 1e3:23, 2e3:24, 5e3:25, 10e3:26, 20e3:27, \
                50e3:28, 100e3:29}
        return tc_d

    def tc_revDict(self):
        """
        Returns the floating point time constants corresponding to the 
        integer codes, as detailed in the table in the .tc docstring.
                
        Technically, this reverses the dictionary returned by .tc_dict().
        
        As a shortcut to a single integer code, you can use the syntax 
        
            .tc_revDict()[integer_code] 
            
        to return the floating-point time constant.  E.g.
        
            .tc_revDict()[11]
            
        will return the value 100e-3.
        
        * YOU MUST USE AN INTEGER TYPE as the key if you do this. 1, not 1.0
        
        The entire reverse dictionary is returned if you don't use a specific key.
        """
        tc_dict_fwd = self.tc_dict()  # get the forward dictionary
        tc_dict_rev = {}  # initialize a new reverse dictionary
        for ii in tc_dict_fwd.keys():
            tc_dict_rev[tc_dict_fwd[ii]] = ii  # build the reverse dictionary
        return tc_dict_rev

    def tc_synchronous(self, n=None):
        """
        0 to disable synchronous time constant mode
        1 to enable  "
        blank to return the present setting
        
        Enabling this is most useful below 10 Hz. The actual time constant is 
        not user-selected but is a multiple of the reference signal's period.
        In any case, the time constant will not go below 100 ms when enabled.
        (See manual p. 5-13, i.e. PDF p. 61)
        """
        resp = self.SetOrCheckSomething('SYNC', n, 0, 1, True)
        return resp

    def AutoOffset(self, silence=False):
        """
        This isn't really meant to be called directly, since it requires some
        user action, namely removing the stimulus (covering the detector, turning
        off the driving voltage, etc.)
        
        Set silence = True to NOT print a warning to the shell that you just 
        did something wrong :)
        """
        if not(silence):
            print('You need to zero the input signal before using auto-offset. I hope you did...')
        if self._dualHarm_|self._dualRef_:
            self.raw.write('AXO1')
            self.raw.write('AXO2')
        else:
            self.raw.write('AXO')
    
    def refMode(self, n=None):
        """
        REFMODE [n] Reference mode selector

        n   Mode
        0   Single Reference / Virtual Reference mode
        1   Dual Harmonic mode
        2   Dual Reference mode (i.e. the internal AND external reference)

        NOTE: When in either of the dual reference modes the command set changes to
        accommodate the additional controls. These changes are detailed in section
        6.4.14 of the manual (pp. 6-34+, i.e. PDF pp. 152).
        
        Also NOTE: I am setting the slope to 12 dB/octave away from the default
        value, as is done in .reset().
        """
        if n == 0:
            print('Switching to single harmonic mode...')
        elif n == 1:
            print('Switching to dual harmonic mode (set them using .refHarmonic(m1, m2))...')
        elif n == 2:
            print('Switching to dual reference mode (internal and external)...')
        resp = self.SetOrCheckSomething('REFMODE', n, 0, 2, True)
        # This is pretty important, so if changed, we check this again and save
        # the state in class attributes
        if n is not None:
            resp = self.SetOrCheckSomething('REFMODE', None)
            # Also change the slope of both to 12 dB/octave
            self.slope(1,1)
            print('Note: slope also changed from default value to 12 dB/octave.')
        # Update the attributes either way, now that we have a "resp" for sure.
        self._dualHarm_ = resp=='1'
        self._dualRef_ = resp=='2'
        return resp    

    def refInternal(self):
        """
        Use the internal reference.
        
        complement is .refExternal()
        """
        self.raw.write('IE 0')

    def refExternal(self):
        """
        Use the external front-panel reference.
        
        complement is .refInternal()
        """
        self.raw.write('IE 2')  # note: IE 1 would be the rear-panel, if you need that instead

    def refHarmonic(self, n=None, n2=None):
        """
        REFN [n] Reference harmonic mode control
        
        Select the nth harmonic of the reference signal(s).
        
        The value(s) of n (and n2) can be an integer from 1 to 65535 
        (except in dual reference mode, where the 2nd is limited to 1, the 
        fundamental frequency).
        """
        if self._dualHarm_|self._dualRef_:
            resp1 = self.SetOrCheckSomething('REFN1', n, 1, 65535, True)
            if self._dualRef_: # this additionally constrains n for the 2nd reference
                resp2 = self.SetOrCheckSomething('REFN2', n2, 1, 1, True)
            else: # but it's all good on dual harmonic mode, of course
                resp2 = self.SetOrCheckSomething('REFN2', n2, 1, 65535, True)
            resp = str(resp1)+', '+str(resp2) # concatenate for output
        else:
            resp = self.SetOrCheckSomething('REFN', n, 1, 65535, True)
        return resp
    
    def refFreq(self):
        """
        If using external reference (.refExternal()), this returns a 0 if
        unlocked or a 1 if locked.  
        
        If using the internal reference, the frequency of the reference is
        returned *in floating point units*
        """
        resp = float(self.raw.ask('FRQ.'))
        return resp

    def refPhase(self, phi=None, phi2=None):
        """
        Set the phase of the reference channel(s) in degrees.  
        
        Usually this is set such that the quadrature signal is nulled (Y -> 0)
        
        Separate settings for two channels in dual reference or dual harmonic mode.
        """
        if self._dualHarm_|self._dualRef_:
            resp1 = self.SetOrCheckSomething('REFP1.', phi, -360., 360., True)
            resp2 = self.SetOrCheckSomething('REFP2.', phi2, -360., 360., True)
            resp = str(resp1)+', '+str(resp2) # concatenate for output
        else:
            resp = self.SetOrCheckSomething('REFP.', phi, -360., 360., True)
        # Some how '\x00' seems to end up in here. Take it out
        resp = resp.replace('\x00', '')
        return resp

    def autoPhase(self, ch=1):
        """
        Perform an auto quadrature null operation, shifting the reference phase
        to zero Y (the imaginary component, 90 deg out of phase with ref).
        
        In dual mode, send in a 2, or call .autoPhase2().
        """
        # Force ch to be an integer either 1 or 2
        if (ch != 1) & (ch != 2):
            ch = 1
        # In dual mode, we must define which harmonic or reference
        if self._dualHarm_ | self._dualRef_:
            self.raw.write('AQN'+str(ch))
        else:
            self.raw.write('AQN')
        # Wait for the operation to complete
        self.pollStatusByte()

    def autoPhase2(self):
        """
        Perform an auto quadrature null operation on the second harmonic or 
        reference in dual mode.
        
        Really just calls .autoPhase(2).
        """
        self.autoPhase(2)

    def oscAmplitude(self, Vrms=None):
        """
        Set the internal oscillator amplitude in Vrms, between 0 and 5.000 V.
        
        Leave input blank to return the present value (again, in V_rms)
        
        (For sinusoids, which this is, Vrms = Vp/sqrt(2) and Vp = Vpp/2)
        """
        resp = self.SetOrCheckSomething('OA.', Vrms, 0., 5.)
        return resp

    def oscFreq(self, freqHz=None):
        """
        Set the internal oscillator frequency in Hz, between 0 and 250 kHz.
        (Limited on the top end to 20 kHz in dual harmonic or reference mode.)
        
        Leave input blank to return the present value (again, in Hz)
        """
        if self._dualHarm_ | self._dualRef_:
            freqLim = 20e3
        else:
            freqLim = 2.5e5
        resp = self.SetOrCheckSomething('OF.', freqHz, 0., freqLim)
        return resp
    def dacLevel(self, dacn=1, level=None):
        if level:
            self.raw.write('DAC. '+str(dacn)+' '+str(level))
        else:
            return self.raw.ask('DAC. '+str(dacn))
    def GetN(self, cmdStr, Nmeas=1, numOfValuesPer=1):
        """
        Get N of something, and convert the result to a floating point string.
        
        If you're asking for measurements that return more than one value,
        say so in "numOfValuesPer". Specifying this makes the loop a bit faster,
        I think.
        
        Note that some special stuff must be done here to handle dual harmonic
        or reference mode, not just in the calling functions.
        """
        # Get all the strings first
        resp = []
        for ii in range(Nmeas):
            resp.append(self.raw.ask(cmdStr)) # get next meas as a raw string
        # Now parse it up
        dataOut = np.zeros((Nmeas, numOfValuesPer)) # initialize
        for ii in range(Nmeas):    
            if self._dualHarm_ | self._dualRef_:
                # We sent two commands separated by ; so we get two results back
                # Split it up at the termination char first
                strList_intermed = resp[ii].split(self.raw.term_chars) 
                # Then split the first result at the instrument's separator, a comma by default
                strList = strList_intermed[0].split(',')
                # Now the second result; there should never be more than two (*dual* mode, not tri etc)
                strList.extend(strList_intermed[1].split(',')) 
            else:
                # Split it up at the commas
                strList = resp[ii].split(',')
            #for jj in range(len(strList)):
            for jj in range(numOfValuesPer): # this might be marginally faster
                # Convert the string to a number and store it
                dataOut[ii,jj] = float(strList[jj].replace('\x00', '')) 
                # Note: sometimes a '\x00' character pops in there, usually in dual harm/ref modes
                # If so we get rid of it.
        return dataOut
    
    def getR(self, N=1, allowWarn=True):
        """
        Get N magnitude points in amps or volts (RMS), returned as an Nx1 NumPy
        array (or N x 2 in either dual harmonic or dual reference mode.)
        
        A warning will appear for N>1, but can be silenced by setting 
        allowWarn=False.  See below for more details...
        
        Important note from the 7260 manual: Section 3.3.15 details the many 
        limitations of using the magnitude and phase outputs. An excerpt:
          "These considerations lead to the conclusion that when the magnitude 
          output is being used, the time constants of the demodulator should be 
          set to give the required signalto-noise ratio at the X channel and 
          Y channel demodulator outputs; improving the signal-to-noise ratio by 
          averaging the magnitude output itself is not to be recommended.
        """
        if self._dualHarm_|self._dualRef_:
            dataOut = self.GetN('MAG1.;MAG2.', N, 2)
        else:
            dataOut = self.GetN('MAG.', N, 1)
            dataOut = dataOut.flatten() # Gets rid of the nested array in this case
        if (N > 1) & allowWarn:
            print('Warning: improving the signal-to-noise ratio by averaging the '+\
                  'magnitude output is NOT recommended. See docstring or manual '+\
                  'section 3.3.15 (p. 34)')
        return dataOut

    def getR_theta(self, N=1, allowWarn=True):
        """
        Get the magnitude in amps or volts (RMS), along with the phase in degrees.
        Returned as an Nx2 NumPy array.
        
        A warning will appear for N>1, but can be silenced by setting 
        allowWarn=False.  See .getR help or see below for more details...
        """
        if self._dualHarm_ | self._dualRef_:
            dataOut = self.GetN('MP1.;MP2.', N, 4)
        else:
            dataOut = self.GetN('MP.', N, 2)
        if (N > 1) & allowWarn:
            print('Warning: improving the signal-to-noise ratio by averaging the '+\
                  'magnitude output is NOT recommended. See docstring or manual '+\
                  'section 3.3.15 (p. 34)')
        return dataOut

    def getXY(self, N=1):
        """
        Get the X and Y values in amps or volts (RMS), returned as an N x 2 
        NumPy array.
        """
        if self._dualHarm_ | self._dualRef_:
            dataOut = self.GetN('XY1.;XY2.', N, 4)
        else:
            dataOut = self.GetN('XY.', N, 2)
        return dataOut

    def getXY_wait3tau(self, N=1, printToShell=True):
        """
        Get the X and Y values, but wait 3*tau_max first.  
        
        Returns data just like .getXY
        
        (In dual harmonic/ref modes, uses the maximum time constant.)
        """
        tc_s = self.tc()  # get the instrument time contant(s)
        threeTC = 3. * float(tc_s.split(',')[0])
        # If dual mode, we have 2 different time constants. Find maximum.
        if self._dualHarm_ | self._dualRef_:
            threeTC2 = 3. * float(tc_s.split(',')[1]) # Get other time constant
            threeTC = np.max([threeTC, threeTC2])
        if threeTC < 30.:
            if printToShell:
                print('\nPresent value of X, Y: (waiting 3*tau = ' + str(threeTC) + ' s)\n')
            time.sleep(threeTC)
            xy = self.getXY(N)
            if printToShell:
                if len(xy[0]) == 4: # dual harmonic mode
                    print(' X1, Y1 = ' + format(xy[0][0], '+.3e') + \
                          ', '+format(xy[0][1], '+.3e') + ' V')
                    print(' X2, Y2 = ' + format(xy[0][2],'+.3e') + \
                          ', '+format(xy[0][3], '+.3e') + ' V')
                else:
                    print(' X, Y = ' + format(xy[0][0], '+.3e') + \
                          ', ' + format(xy[0][1], '+.3e') + ' V')
        else:
            # We're not going to wait 3 time constants, just give up.
            xy=None
            print('\nYour time constants are quite long. I\'m not going to wait '+\
                  'around for the outputs to settle.')
        return xy